﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Practices.ObjectBuilder2;
using Microsoft.Practices.Unity;
using TestByWire.Factories;

namespace TestByWire.BuildStrategies
{
	internal class AutoReturnBuildStrategy : BuilderStrategy
	{
		private readonly IStubInstanceFactory _stubInstanceFactory;
		private readonly IUnityContainer _container;
		private readonly PipelineSettings _pipelineSettings;
		private object _instanceBeingStubbed;

		public AutoReturnBuildStrategy(IUnityContainer container, IStubInstanceFactory stubInstanceFactory, Type typeUnderTest)
		{
			if (container == null)
			{
				throw new ArgumentNullException("container");
			}

			if (stubInstanceFactory == null)
			{
				throw new ArgumentNullException("stubInstanceFactory");
			}

			if (typeUnderTest == null)
			{
				throw new ArgumentNullException("typeUnderTest");
			}

			_container = container;
			_stubInstanceFactory = stubInstanceFactory;
			_pipelineSettings = _container.Resolve<PipelineSettings>();
		}

		public override void PreBuildUp(IBuilderContext context)
		{
			if (context == null)
			{
				throw new ArgumentNullException("context");
			}

			if (_pipelineSettings.OmmitReturnStubsForThese.Contains(context.OriginalBuildKey.Type))
			{
				return;
			}
			var nextStub = new DoNotStub(context.BuildKey.Type, context.BuildKey.Name);
			if (!context.BuildComplete &&
				!_pipelineSettings.DoNotStubList.Contains(nextStub) &&
				nextStub.IsInContainer(_container))
			{
				_instanceBeingStubbed = null;

				ApplyAutoReturnTypes(context);
			}
		}

		protected internal virtual void SetupStubReturnValue(object instanceBeingStubbed, Expression<Action<object>> action, object stubbedReturnInstance)
		{
			Scenario.Is(() => A.PriorityCallTo(instanceBeingStubbed, action, Priority.Low).Returns(stubbedReturnInstance));
		}

		private void ApplyAutoReturnTypes(IBuilderContext context)
		{
			var query = context.BuildKey.Type.GetMethods();
			foreach (var methodInfo in query)
			{
				if (StubReturnType(methodInfo))
				{
					StubCreationResult stubCreationResult = _stubInstanceFactory.CreateStubInstanceOf(methodInfo.ReturnType, context.OriginalBuildKey.Name);
					Debug.Assert(stubCreationResult != null);

					if (MethodIsPropertyWithPublicSetter(methodInfo))
					{
						var setterMethod = GetPropertySetterMethodInfo(methodInfo);
						var instance = GetInstanceBeingStubbed(context);
						var action = _stubInstanceFactory.CreatePropertySetAction(instance, setterMethod, stubCreationResult.Instance);
						SetupStubReturnValue(instance, action, stubCreationResult.Instance);
					}
					else
					{
						var action = _stubInstanceFactory.CreateStubAction(GetInstanceBeingStubbed(context), methodInfo);
						SetupStubReturnValue(GetInstanceBeingStubbed(context), action, stubCreationResult.Instance);
					}
				}
			}
		}

		private MethodInfo GetPropertySetterMethodInfo(MethodInfo propertyGetterMethodInfo)
		{
			var expectedSetterName = propertyGetterMethodInfo.Name.Remove(0, 3).Insert(0, "set");

			return propertyGetterMethodInfo.DeclaringType.GetMethod(expectedSetterName);
		}

		private bool MethodIsPropertyWithPublicSetter(MethodInfo methodInfo)
		{
			if (!methodInfo.Name.StartsWith("get_", StringComparison.OrdinalIgnoreCase))
				return false;

			return GetPropertySetterMethodInfo(methodInfo) != null;
		}

		private static Boolean StubReturnType(MethodInfo mi)
		{
			if (!mi.ReturnType.IsInterface)
				return false;
			return mi.DeclaringType != typeof(Object);
		}


		private object GetInstanceBeingStubbed(IBuilderContext context)
		{
			if (_instanceBeingStubbed == null)
			{
				_instanceBeingStubbed = _container.Resolve(context.OriginalBuildKey.Type, context.OriginalBuildKey.Name);
			}

			return _instanceBeingStubbed;
		}
	}
}
